package agents.resource;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import agents.exceptions.ActionFailedException;
import agents.resource.exceptions.BookingFailedException;
import agents.resource.exceptions.CollisionException;
import domain.Car;
import domain.Clock;
import domain.CrossRoad;
import domain.TimeInterval;

public class CrossRoadAgent extends ResourceAgent {
	
	private final CrossRoad crossroad;
	
	public CrossRoadAgent(CrossRoad crossroad) {
		super();
		this.crossroad = crossroad;
		this.ingoingResources = new ArrayList<ResourceAgent>();
		this.outgoingResources = new ArrayList<ResourceAgent>();
	}
	
	public CrossRoadAgent(CrossRoad crossroad, Collection<ResourceAgent> ingoing, Collection<ResourceAgent> outgoing) {
		this(crossroad);
		if(ingoing == null || outgoing == null) throw new IllegalArgumentException();
		this.ingoingResources = new ArrayList<ResourceAgent>(ingoing);
		this.outgoingResources = new ArrayList<ResourceAgent>(outgoing);
		for(ResourceAgent in : ingoing) {
			in.addOutgoingResource(this);
		}
		for(ResourceAgent out : outgoing) {
			out.addIngoingResource(this);
		}
	}
	
	@Override
	public CrossRoad getTrafficInfrastructure() {
		return crossroad;
	}
	
	//beweegt op kruispunt naar opgegeven uitgangsweg
	//beweegt auto naar volgend kruispunt (2 kruispunten naar elkaar kan, 2 straten niet)
	public void moveTo(Car car, ResourceAgent neighbour) throws ActionFailedException {
		if(isRemoved()) throw new ActionFailedException();
		if(!getOutgoingResources().contains(neighbour)) throw new IllegalArgumentException("slechte resource opgegeven");
		if(!hasCar(car)) throw new IllegalArgumentException();
		try {
			neighbour.addCar(this.getTrafficInfrastructure().getSegment().getCar());
		} catch (CollisionException e) {
			throw new ActionFailedException(e);
		}
	}
	
	//geeft alle nog niet geboekte intervals terug, vanaf een bepaalde tijd
	@Override
	public List<TimeInterval> getFreeIntervals(long startTime, Car car) {
		if(getBookedExitCar() != null && startTime < getBookedExitTime()) {
			startTime = getBookedExitTime()+1;
		}
		long firstTime = startTime;
		List<TimeInterval> result = new ArrayList<TimeInterval>();
		List<Booking> bookings = getAllBookingsNotFrom(car);
		
		int i = 0;
		for(; i < bookings.size(); i++){
			if(bookings.get(i).getTimeInterval().contains(startTime)){
				firstTime = bookings.get(i).getTimeInterval().getEndTime()+1;
				i++;
				break;
			}
			if(bookings.get(i).getTimeInterval().getStartTime() > startTime){
				break;
			}
		} //i staat nu op eerste boeking na het eerste vrije interval, firstTime is begintijd van eerste vrije interval
		
		for(; i < bookings.size(); i++){
			if(firstTime != bookings.get(i).getTimeInterval().getStartTime()) { //als er een gaatje tussen 2 boekingen is
				result.add(new TimeInterval(firstTime, bookings.get(i).getTimeInterval().getStartTime()-1));
			}
			firstTime = bookings.get(i).getTimeInterval().getEndTime()+1;
		}
		result.add(new TimeInterval(firstTime));
		return result;
	}
	
	
	@Override
	public TimeInterval getPossibleExitInterval(long enterTime, Car car) {
		List<Booking> bookings = getAllBookingsNotFrom(car);
		for(Booking b : bookings) {
			if(b.getTimeInterval().getStartTime() > enterTime) {
				return new TimeInterval(enterTime, b.getTimeInterval().getStartTime()-1);
			}
		}
		return new TimeInterval(enterTime);
	}
	
	//tijdsinterval dat auto op kruispunt het kan/moet verlaten
	@Override
	public TimeInterval getDesiredExitTime(Car car){
		long now = Clock.getInstance().getCurrentTime();
		List<Booking> nextBookings = getAllBookingsNotFrom(car);
		if(nextBookings.size() == 0) { //er zijn geen andere boekingen
			return new TimeInterval(now);
		} else {	//er zijn andere boekingen
			Booking firstBooking = nextBookings.get(0);
			long timeFirst = Math.max(firstBooking.getTimeInterval().getStartTime()-1,now);
			return new TimeInterval(now,timeFirst);
		}
	}

	@Override
	public void addCar(Car car) throws CollisionException {
//		check of er geen auto op het kruispunt staat
		if(getTrafficInfrastructure().getSegment().getCar() != null)
			throw new CollisionException();
		else
			car.move(getTrafficInfrastructure().getSegment());
	}
	
	/* *** inkomende en uitgaande wegen *** */
	
	private List<ResourceAgent> outgoingResources;
	private List<ResourceAgent> ingoingResources;

	@Override
	public List<ResourceAgent> getOutgoingResources() {
		return new ArrayList<ResourceAgent>(outgoingResources);
	}

	@Override
	public List<ResourceAgent> getIngoingResources() {
		return new ArrayList<ResourceAgent>(ingoingResources);
	}
	
	@Override
	void addOutgoingResource(ResourceAgent out){
		outgoingResources.add(out);
	}
	
	@Override
	void addIngoingResource(ResourceAgent in){
		ingoingResources.add(in);
	}
	
	@Override
	void removeIngoingResource(ResourceAgent in) {
		ingoingResources.remove(in);
	}

	@Override
	void removeOutgoingResource(ResourceAgent out) {
		outgoingResources.remove(out);		
	}

	@Override
	public long getNoTrafficExitTime(Car car) {
		return Clock.getInstance().getCurrentTime()-1;
	}

	@Override
	public void bookExitResource(long exitTime, Car car) throws BookingFailedException {
		if(car == null || ! hasCar(car)) {
			throw new IllegalArgumentException("car die exit boekt moet zich op de resource bevinden");
		}
		if(exitTime < Clock.getInstance().getCurrentTime()) throw new IllegalArgumentException("boeking in verleden");
		bookedExitTime = exitTime;
		bookedExitCar = car;
		shiftConflictingBookings();
	}

	@Override
	protected long getLastBookedExitTime() {
		if(getBookedExitCar() == null) { //als er geen exit booking is
			return Clock.getInstance().getCurrentTime() - 1;
		}
		return getBookedExitTime();
	}

	private long bookedExitTime;
	private Car bookedExitCar; //auto van wie de exit time booking is

	@Override
	public long getFirstExitTimeOf(Car car) {
		return getNoTrafficExitTime(car);
	}

	private long getBookedExitTime() {
		return bookedExitTime;
	}

	private Car getBookedExitCar() {
		if(bookedExitCar != getTrafficInfrastructure().getSegment().getCar()) bookedExitCar = null;
		return bookedExitCar;
	}
	
	//schuif alle bookings op totdat deze niet meer conflicteren met de exit times van auto's op de resource
	protected void shiftConflictingBookings() {
		long lastExitTime = getLastBookedExitTime();
		int bookingIndex = 0;
		for(Booking b : getAllBookings()) {
			long diff = (lastExitTime + 1) - b.getTimeInterval().getStartTime();
			lastExitTime = b.getTimeInterval().getEndTime(); //update last exit time 
			if(diff > 0) { //booking aanpassen als deze conflicteert met vorige last exit time
				TimeInterval newBookingTime = b.getTimeInterval().shift(diff);
				Booking newBooking = new Booking(newBookingTime,b.getCar()); 
				removeBooking(b);			
				addBooking(bookingIndex, newBooking);
				lastExitTime = newBooking.getTimeInterval().getEndTime(); //ook last exit time shiften
			}
						
			bookingIndex++;
		}
	}

	@Override
	protected boolean hasExitBooking(Car car) {
		return getBookedExitCar() == car;
	}
}